Skip to content

feat(app): add a session media panel behind the session_media flag - #62217

Open
neriousy wants to merge 8 commits into
devfrom
session-media
Open

neriousy wants to merge 8 commits into
devfrom
session-media

Conversation

@neriousy

@neriousy neriousy commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a Media tab to the session side panel that lists the images, videos and links shared in a session, by you or the agent, so you can find a screenshot or a link without scrolling the conversation. It covers the whole session, not just the messages the timeline has loaded.

  • Media: images and videos you attached or pasted, images the agent read or wrote by path, and images tools returned, such as browser screenshots. Shown as gallery cards grouped by day.
  • Gallery viewer: clicking an image opens the existing zoomable viewer as a gallery over the tab's images. Previous/next buttons and a "3 / 12" counter sit in its control bar, and ← / → step while the image isn't zoomed in (mirrored in right-to-left layouts). Images whose files are gone stay as "Couldn't load" cards but are skipped by the viewer.
  • Links: web addresses in your messages and the agent's replies, pages the agent fetched, and web search results, each listed once. They're grouped by site, or by repository for GitHub. Issues and pull requests show as #123 chips, and other pages as title chips. Hovering a chip shows the full title, the address, who shared it and when. Clicking opens the in-app browser, or the system browser when that isn't available. Icons work like the timeline's links: GitHub's mark, a public site's favicon, and a globe for private hosts such as localhost, which are never sent to the favicon service.
  • Search: a search button opens a field under the filters that matches link titles and addresses and image names and paths. While searching, the panel keeps reading older history so it finds items not listed yet.
  • Filters: Media / Links, and an Everyone / You / Agent dropdown for who shared it. Both are saved per session, so they stay the same when you switch tabs or restart the app.
  • Loading: older history loads as you scroll to the end of the list. Thumbnails load as they scroll into view.

Implementation

Server: a built-in plugin, packages/core/src/plugin/media.ts, registered next to goal. It serves an experimental.media RPC whose list method reads a page of a session's messages from the database, newest first, and returns only the images, videos and links in them, plus a cursor for the next page. Pages never include file bytes. The extraction is in media/collect.ts, with tests in packages/core/test/plugin/media.test.ts. The RPC definition and item schemas are in packages/schema/src/session-media.ts.

App: the built-in GUI plugin in packages/gui-plugins/src/media/.

  • history.ts pages the RPC: 200 messages on the first read, 200 more each time older history is needed, and the 50 newest whenever the timeline's last message changes (a new message, a tool finishing, a reply ending). It doesn't read message contents from the timeline or make the timeline load older history.
  • source.ts loads each card's bytes once it's visible: stored media through experimental.media.get, files by path through file.read, and inline bytes from their message.
  • link-groups.ts groups the links, with tests. links.tsx renders the groups.

ImagePreview (packages/ui) gains two optional props, position and onStep, for stepping through a set. It now remembers which origin it hid, so stepping to another image shows the thumbnail it left. Existing callers pass neither prop and behave as before.

Feature flag

Guarded by the new session_media flag in packages/schema/src/config/flag.ts. It is on in local and dev builds and off in releases. When the flag is off, the server doesn't register the RPC, and the app closes the tab and hides the + menu item and the "Open media" command.

The app reads the flag from the session directory's config the same way Core resolves it: experimental.session_media, then experimental.all_flags, then the build channel. It can't see OPENCODE_FLAGS, which only the server reads.

Not included

  • Clicking an item can't jump to the message it came from yet, because the plugin SDK has no way to scroll the conversation to a message.
  • An image whose bytes are inline in a message (a pasted image, a tool screenshot) loads that whole message to show its thumbnail.
  • The panel is hidden on narrow screens.

Testing

  • bun run check passes, plus bun test src/media in packages/gui-plugins, the media plugin tests in packages/core, and the image tests in packages/ui.
  • Called the RPC on a dev server running against a copy of a real database: paging with the cursor reaches the start of a session, a pasted image and agent screenshots come back as references without bytes, those references load from their messages, an unknown session returns an empty page, and an out-of-range page size is rejected.
  • The redesigned links, search and saved filters have not been checked in a running app yet.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" color="currentColor">
  <style>
    .t{animation:in 3.2s ease-out infinite both}
    .t2{animation-delay:.25s}.t3{animation-delay:.5s}.t4{animation-delay:.75s}
    @keyframes in{0%{opacity:0}15%,85%{opacity:1}100%{opacity:1}}
    @media (prefers-reduced-motion:reduce){.t{animation:none}}
  </style>
  <rect x="8" y="10" width="92" height="52" rx="6" stroke="currentColor" stroke-opacity=".35"/>
  <rect x="14" y="16" width="56" height="4" rx="2" fill="currentColor" fill-opacity=".35"/>
  <rect x="14" y="26" width="72" height="4" rx="2" fill="currentColor" fill-opacity=".2"/>
  <rect x="14" y="36" width="64" height="4" rx="2" fill="currentColor" fill-opacity=".2"/>
  <rect x="44" y="46" width="48" height="10" rx="3" fill="currentColor" fill-opacity=".15"/>
  <rect x="108" y="10" width="124" height="52" rx="6" stroke="currentColor" stroke-opacity=".35"/>
  <rect x="114" y="15" width="44" height="7" rx="3" fill="currentColor" fill-opacity=".18"/>
  <rect x="200" y="15" width="26" height="7" rx="3" fill="currentColor" fill-opacity=".12"/>
  <g class="t"><rect x="114" y="27" width="25" height="19" rx="3" fill="currentColor" fill-opacity=".3"/><rect x="114" y="49" width="18" height="3" rx="1.5" fill="currentColor" fill-opacity=".25"/></g>
  <g class="t t2"><rect x="142" y="27" width="25" height="19" rx="3" fill="currentColor" fill-opacity=".22"/><rect x="142" y="49" width="14" height="3" rx="1.5" fill="currentColor" fill-opacity=".25"/></g>
  <g class="t t3"><rect x="170" y="27" width="25" height="19" rx="3" fill="currentColor" fill-opacity=".3"/><rect x="170" y="49" width="20" height="3" rx="1.5" fill="currentColor" fill-opacity=".25"/></g>
  <g class="t t4"><rect x="198" y="27" width="28" height="19" rx="3" fill="currentColor" fill-opacity=".22"/><rect x="198" y="49" width="16" height="3" rx="1.5" fill="currentColor" fill-opacity=".25"/></g>
</svg>

Added by the opencode bot (AI-generated).

Related: #58295

Public API: adds the experimental.session_media flag, an experimental experimental.media RPC with its @opencode/schema/session-media schemas, and optional ImagePreview position/onStep props (backwards compatible)

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't run the panel: it only exists in the web/desktop app, and this environment has no browser (only the TUI can be driven here). Could you attach a couple of screenshots or a short video of the Media tab and the gallery viewer?

The new collect tests and the full gui-plugins suite pass, and so do the ui image tests. tsc is clean in gui-plugins, ui, schema and session-ui. From reading the code, the flag check mirrors Core's Flag.resolve order, and existing ImagePreview callers work as before. I found no blocking issues.

Public API, all backwards compatible:

  • Adds the optional boolean config key experimental.session_media (config schema, OpenAPI spec and generated client types).
  • Adds a new @opencode/session-ui/sources/site-icon export.
  • Adds two optional props, position and onStep, to ImagePreview.

One thing to keep in mind: enabled() in media/renderer.tsx is an app-side copy of Core's flag resolution and can't see OPENCODE_FLAGS. If more app features get flags, the server could expose resolved flags so the two can't drift.

Related: #58295

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit only changes how the Files and Links lists look (rows grouped by day, a shared row layout, who-did-what labels and the item's folder or address), and I found nothing new to flag. bun test src/media and tsc -b in packages/gui-plugins both pass. I couldn't render the panel here because there's no browser in this environment. A screenshot of the new file and link rows would help reviewers.

The public API is the same as in the last review. This commit only touches the plugin's UI, CSS and English strings.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit replaces the "Load earlier messages" button with automatic loading as the panel scrolls. bun test src/media and tsc -b in packages/gui-plugins pass. I couldn't watch the scroll behaviour here because there's no browser in this environment.

One concern is worth fixing: when the open tab has few or no items, the automatic loading never stops until the session's whole history is in the app's message store (see the inline comment). The public API hasn't changed since the last review.


// Syncs with the server: one page at a time while the end is visible, so it stops once a page pushes the end off
// screen, the history ends, or a load fails.
createKeyed(() => end() && more() && !loading() && !failed() && `${props.session.id}:${settled()}`, page)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three tabs read the same messages. If the open tab has few or no items (say Links in a session without links), the end marker never scrolls off screen, so this keeps calling loadMore until more() is false. In a long session, just opening the panel pulls the whole history into the app's shared message store, which works against the app keeping only recent messages and puts that history into the store the timeline also uses. Could automatic loading stop after a few pages, or only run when the user scrolls, with a button for the rest?

Separately, failed doesn't seem to reset when props.session changes. If so, after an error the next session won't load older messages until "Try again" is clicked.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverting the automatic loading addresses my earlier concern: the panel only loads older messages when "Load older" is clicked, so opening it can't pull a session's whole history into memory, and the failed state that didn't reset between sessions is gone with it. bun test src/media and tsc -b in packages/gui-plugins pass. I couldn't render the panel here because there's no browser in this environment. Nothing new to raise, and the public API hasn't changed since the last review.

The Media panel now lists a session's images, videos and links through an experimental.media RPC served by a built-in plugin behind session_media, paging the whole history newest first instead of reading the timeline's loaded messages. Links group by site or GitHub repository with search, the Files tab is gone, and the panel keeps its tab and sender filter per session.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the session's media through the new server RPC addresses my earlier concern. Older pages come from experimental.media.list into the panel's own history, so the end-of-list loading no longer fills the timeline's message store, and the failure flag starts fresh with each session's history. The new collect tests in packages/core and the link-groups test in packages/gui-plugins pass, and tsc is clean in core, gui-plugins and schema. I couldn't render the panel here because there's no browser in this environment, so a short video of the links view, search and paging would help.

Public API changes since the last review, all backwards compatible:

  • Adds the experimental.media RPC with a list({ sessionID, cursor?, limit? }) method, marked experimental and registered only when session_media is on.
  • Adds a new @opencode/schema/session-media module with the Source, Media, Link and Page schemas.
  • Removes the @opencode/session-ui/sources/site-icon export that earlier versions of this pull request added.

The experimental.session_media config key and the ImagePreview position and onStep props are unchanged. Nothing new to raise.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant